<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h3 data-lake-id="8e1b944f" id="8e1b944f"><span data-lake-id="ud703819b" id="ud703819b">背景</span></h3>
  <p data-lake-id="u751096c8" id="u751096c8"><br></p>
  <p data-lake-id="ud274d8f4" id="ud274d8f4"><span data-lake-id="u65deedbe" id="u65deedbe">在一个典型的业务场景中，我们提供了一个API ，被用来允许外部或内部客户端调用。这些 API 可能会暴露敏感数据或业务逻辑，因此需要确保只有授权的用户才能访问。同时，为了防止系统过载，需要对 API 调用进行限制。</span></p>
  <p data-lake-id="ua0999523" id="ua0999523"><br></p>
  <h3 data-lake-id="15a472cc" id="15a472cc"><span data-lake-id="u3c3d3d4e" id="u3c3d3d4e">技术选型</span></h3>
  <p data-lake-id="uaf912ed0" id="uaf912ed0"><br></p>
  <ol list="u2dafa660">
   <li fid="u0fb7f48c" data-lake-id="ue4fd71f7" id="ue4fd71f7"><strong><span data-lake-id="u6752e3c3" id="u6752e3c3">API 认证</span></strong><span data-lake-id="u3b53dfcd" id="u3b53dfcd">：使用 API 密钥或 OAuth 令牌。</span></li>
   <li fid="u0fb7f48c" data-lake-id="u8a357a0a" id="u8a357a0a"><strong><span data-lake-id="u2a7e901e" id="u2a7e901e">API 限流</span></strong><span data-lake-id="ucb5d13c5" id="ucb5d13c5">：使用滑动窗口算法实现限流。</span></li>
   <li fid="u0fb7f48c" data-lake-id="ub8775c2f" id="ub8775c2f"><strong><span data-lake-id="ua7993d7b" id="ua7993d7b">限流存储</span></strong><span data-lake-id="u5617d239" id="u5617d239">：使用 Redis 作为存储和计算滑动窗口的工具。</span></li>
  </ol>
  <p data-lake-id="ubeef2318" id="ubeef2318"><br></p>
  <h3 data-lake-id="380cf2b5" id="380cf2b5"><span data-lake-id="u799eaf14" id="u799eaf14">具体实现</span></h3>
  <p data-lake-id="ud2988f07" id="ud2988f07"><br></p>
  <h4 data-lake-id="6e9bb071" id="6e9bb071"><span data-lake-id="u388cd10a" id="u388cd10a">API 认证</span></h4>
  <p data-lake-id="u41288c61" id="u41288c61"><br></p>
  <ol list="u534ed700">
   <li fid="uad4154cd" data-lake-id="u0db1a4a7" id="u0db1a4a7"><strong><span data-lake-id="u59b1ec05" id="u59b1ec05">生成 API 密钥</span></strong><span data-lake-id="u614fea70" id="u614fea70">：为每个用户生成唯一的 API 密钥。当用户创建账户时，后端生成密钥并提供给用户。</span></li>
   <li fid="uad4154cd" data-lake-id="u8d0884d3" id="u8d0884d3"><strong><span data-lake-id="ubad1614e" id="ubad1614e">客户端请求</span></strong><span data-lake-id="ucb441005" id="ucb441005">：客户端在发起请求时需在 HTTP 头部附带 API 密钥。</span></li>
   <li fid="uad4154cd" data-lake-id="ufbf3624e" id="ufbf3624e"><strong><span data-lake-id="uc71105a1" id="uc71105a1">服务器端验证</span></strong><span data-lake-id="u398c351b" id="u398c351b">：服务器接收到请求后，提取并验证 API 密钥。如果密钥无效或缺失，请求将被拒绝。</span></li>
  </ol>
  <p data-lake-id="u1565b959" id="u1565b959"><br></p>
  <pre lang="java"><code>
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class ApiKeyAuthenticationFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        String apiKey = httpRequest.getHeader("X-API-KEY");
        if (!isValidApiKey(apiKey)) {
            httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            httpResponse.getWriter().write("Unauthorized");
            return;
        }
        chain.doFilter(request, response);
    }

    private boolean isValidApiKey(String apiKey) {
        // 实现 API 密钥的验证逻辑
        return true; 
    }
}

</code></pre>
  <p data-lake-id="udaf7182a" id="udaf7182a"><br></p>
  <h4 data-lake-id="522e2149" id="522e2149"><span data-lake-id="u47fa5097" id="u47fa5097">API 限流</span></h4>
  <p data-lake-id="u731bd68d" id="u731bd68d"><br></p>
  <ol list="uea5a72e3">
   <li fid="u9f65ce98" data-lake-id="u6895321c" id="u6895321c"><strong><span data-lake-id="u514a4fde" id="u514a4fde">滑动窗口算法</span></strong><span data-lake-id="uf58d2450" id="uf58d2450">：使用 Redis 来存储和计算每个用户的请求计数。</span></li>
   <li fid="u9f65ce98" data-lake-id="u159475f5" id="u159475f5"><strong><span data-lake-id="u314792de" id="u314792de">请求计数</span></strong><span data-lake-id="u1af7ba1e" id="u1af7ba1e">：每个请求到达时，使用 Redis 记录该请求的时间戳。</span></li>
   <li fid="u9f65ce98" data-lake-id="ucd470b7d" id="ucd470b7d"><strong><span data-lake-id="ua47097d3" id="ua47097d3">窗口计算</span></strong><span data-lake-id="u50f36c30" id="u50f36c30">：检查当前时间窗口内的请求数量，如果超过阈值，则拒绝请求。</span></li>
  </ol>
  <p data-lake-id="u537e9f37" id="u537e9f37"><br></p>
  <pre lang="java"><code>
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Component
public class RateLimitingFilter implements Filter {

    @Autowired
    SlidingWindowRateLimiter limiter;

    private final RedisTemplate&lt;String, String&gt; redisTemplate;
    private static final int LIMIT = 100; // 设置每分钟的请求限制
    private static final int WINDOW_SIZE_IN_SECONDS = 60; // 时间窗口大小

    public RateLimitingFilter(RedisTemplate&lt;String, String&gt; redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        String apiKey = httpRequest.getHeader("X-API-KEY");
        String key = "rate_limit:" + apiKey;
        long currentTime = Instant.now().getEpochSecond();
        long windowStart = currentTime - WINDOW_SIZE_IN_SECONDS;

        boolean result = limiter.allowRequest(key);

        if (result) {
            httpResponse.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS);
            httpResponse.getWriter().write("Too Many Requests");
            return;
        }

        chain.doFilter(request, response);     
    }
}

</code></pre>
  <p data-lake-id="u5f9bf7de" id="u5f9bf7de"><br></p>
  <p data-lake-id="u2eeb88d7" id="u2eeb88d7"><span data-lake-id="u763d9e12" id="u763d9e12">这里面的SlidingWindowRateLimiter实现如下：</span></p>
  <p data-lake-id="ue0c7ccd9" id="ue0c7ccd9"><span data-lake-id="ua81e6037" id="ua81e6037">​</span><br></p>
  <pre lang="java"><code>
import redis.clients.jedis.Jedis;

public class SlidingWindowRateLimiter {
    private Jedis jedis;
    private String key;
    private int limit;

    public SlidingWindowRateLimiter(Jedis jedis, String key, int limit) {
        this.jedis = jedis;
        this.key = key;
        this.limit = limit;
    }

    public boolean allowRequest(String key) {
        // 当前时间戳
        long currentTime = System.currentTimeMillis();

        // 使用Lua脚本来确保原子性操作
        String luaScript = "local window_start = ARGV[1] - 60000\n" +
                           "redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', window_start)\n" +
                           "local current_requests = redis.call('ZCARD', KEYS[1])\n" +
                           "if current_requests &lt; tonumber(ARGV[2]) then\n" +
                           "    redis.call('ZADD', KEYS[1], ARGV[1], ARGV[1])\n" +
                           "    return 1\n" +
                           "else\n" +
                           "    return 0\n" +
                           "end";

        Object result = jedis.eval(luaScript, 1, key, String.valueOf(currentTime), String.valueOf(limit));
        
        return (Long) result == 1;
    }
}

</code></pre>
  <p data-lake-id="uc1767229" id="uc1767229"><br></p>
  <h3 data-lake-id="bfc25f38" id="bfc25f38"><span data-lake-id="u4ab8d4c8" id="u4ab8d4c8">其他安全和性能考虑</span></h3>
  <p data-lake-id="ubced4487" id="ubced4487"><br></p>
  <ul list="ua4b99f53">
   <li fid="u2b5a5ba2" data-lake-id="u264dc6b0" id="u264dc6b0"><strong><span data-lake-id="u62af13b0" id="u62af13b0">API 密钥安全性</span></strong><span data-lake-id="uec1ff200" id="uec1ff200">：确保 API 密钥通过安全的方式传输（例如 HTTPS）。</span></li>
   <li fid="u2b5a5ba2" data-lake-id="u67559ab1" id="u67559ab1"><strong><span data-lake-id="u1db7bb55" id="u1db7bb55">密钥旋转和管理</span></strong><span data-lake-id="u7eabdbd3" id="u7eabdbd3">：提供机制允许用户定期更换 API 密钥。</span></li>
   <li fid="u2b5a5ba2" data-lake-id="u166de18a" id="u166de18a"><strong><span data-lake-id="u8e90469f" id="u8e90469f">错误处理和日志</span></strong><span data-lake-id="u9a23479f" id="u9a23479f">：合理记录错误和请求日志，以便于问题追踪和分析。</span></li>
  </ul>
  <p data-lake-id="u7db56086" id="u7db56086"><br></p>
  <p data-lake-id="ue3a2a073" id="ue3a2a073"><span data-lake-id="ue29dbf39" id="ue29dbf39">通过这种结合了 API 密钥认证和滑动窗口限流的策略，可以有效提高 API 的安全性和稳定性，防止滥用和系统过载，同时保证合法用户的正常访问。</span></p>
  <p data-lake-id="u19470124" id="u19470124"><span data-lake-id="ud5429767" id="ud5429767">​</span><br></p>
  <h3 data-lake-id="aUdMm" id="aUdMm"><span data-lake-id="u0258e15e" id="u0258e15e">学习资料</span></h3>
  <p data-lake-id="uac1bd769" id="uac1bd769"><br></p>
  <p data-lake-id="u59046399" id="u59046399"><br></p>
  <p data-lake-id="u614dd6b6" id="u614dd6b6"><br></p>
 </body>
</html>